<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Loading Progress</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
    <link rel='stylesheet'
          href='https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,100..1000&amp;display=swap'>
    <style>
        * {
            border: 0;
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }

        :root {
            --hue: 223;
            --bg: hsl(var(--hue), 10%, 90%);
            --fg: hsl(var(--hue), 10%, 10%);
            --gray1: hsl(var(--hue), 10%, 60%);
            --gray2: hsl(var(--hue), 10%, 40%);
            --neutral: hsl(var(--hue), 10%, 50%);
            --success: hsl(153, 90%, 35%);
            --warning: hsl(33, 90%, 50%);
            --trans-dur: 0.3s;
            --trans-timing: cubic-bezier(0.65, 0, 0.35, 1);
            font-size: clamp(1rem, 0.95rem + 0.25vw, 1.25rem);
        }

        body {
            background-color: var(--bg);
            color: var(--fg);
            display: flex;
            font: 1em/1.5 "DM Sans", sans-serif;
            height: 100vh;
            transition: background-color var(--trans-dur), color var(--trans-dur);
        }

        main {
            display: flex;
            overflow-x: hidden;
            width: 100vw;
            height: 100vh;
        }

        svg polyline {
            transition: stroke var(--trans-dur);
        }

        .icon {
            display: block;
            overflow: visible;
            width: 1.5em;
            height: 1.5em;
            transition: color var(--trans-dur);
        }

        .icon--neutral {
            color: var(--neutral);
        }

        .icon--success {
            color: var(--success);
        }

        .icon--warning {
            color: var(--warning);
        }

        .loading {
            display: flex;
            overflow: hidden;
            height: 100%;
        }

        .loading--done {
            overflow: visible;
            height: 26.25em;
        }

        .loading, .loading__step, .loading__steps {
            width: 100%;
        }

        .loading, .loading__steps {
            margin: auto;
        }

        .loading, .loading__step {
            display: flex;
        }

        .loading__ellipsis {
            display: inline-flex;
        }

        .loading__ellipsis-dot {
            --dot-dur: 2s;
            animation: ellipsis-dot-1 var(--dot-dur) steps(1, end) infinite;
            visibility: hidden;
        }

        .loading__ellipsis-dot:nth-child(2) {
            animation-name: ellipsis-dot-2;
        }

        .loading__ellipsis-dot:nth-child(3) {
            animation-name: ellipsis-dot-3;
        }

        .loading__step {
            gap: 1em;
            padding: 0 1.5em;
            position: absolute;
            top: 0;
            left: 0;
            height: 5.25em;
            transition: opacity var(--trans-dur), transform var(--trans-dur) var(--trans-timing);
        }

        .loading__step-info {
            color: var(--gray2);
            font-size: 0.75em;
            line-height: 1.333;
            opacity: 0;
            transition: color var(--trans-dur), opacity var(--trans-dur);
        }

        .loading__step--in .loading__step-info {
            opacity: 1;
        }

        .loading__step-title {
            font-size: 1.25em;
            font-weight: 500;
            line-height: 1.2;
            margin-bottom: 0.25rem;
        }

        .loading__steps {
            position: relative;
            height: 2.75em;
            max-width: 27em;
        }

        /* Dark theme */
        @media (prefers-color-scheme: dark) {
            :root {
                --bg: hsl(var(--hue), 10%, 10%);
                --fg: hsl(var(--hue), 10%, 90%);
            }

            .loading__step-info {
                color: var(--gray1);
            }
        }

        /* Animations */
        @keyframes ellipsis-dot-1 {
            from {
                visibility: hidden;
            }
            25%, to {
                visibility: visible;
            }
        }

        @keyframes ellipsis-dot-2 {
            from, 25% {
                visibility: hidden;
            }
            50%, to {
                visibility: visible;
            }
        }

        @keyframes ellipsis-dot-3 {
            from, 50% {
                visibility: hidden;
            }
            75%, to {
                visibility: visible;
            }
        }
    </style>
</head>
<body>
<div id="root"></div>

<script type="module">
    import React, {StrictMode, useEffect, useRef, useState} from "https://esm.sh/react";
    import {createRoot} from "https://esm.sh/react-dom/client";

    // 创建根节点
    const root = createRoot(document.getElementById('root'));
    root.render(
        React.createElement(StrictMode, null,
            React.createElement("main", null,
                React.createElement(IconSprites, null),
                React.createElement(Loading, null)
            )
        )
    );

    // 图标组件：根据状态显示不同图标
    function Icon({icon, color}) {
        const colorClass = color ? ` icon--${color}` : "";
        return (
            React.createElement("svg", {
                    className: `icon${colorClass}`,
                    width: "16px",
                    height: "16px",
                    "aria-hidden": "true"
                },
                React.createElement("use", {href: `#${icon}`})
            )
        );
    }

    // SVG 图标集合组件
    function IconSprites() {
        const viewBox = "0 0 16 16";
        const emptyCircleRotations = [];

        // 生成空心圆的多个小矩形（模拟虚线效果）
        for (let r = 0; r < 16; ++r) {
            emptyCircleRotations.push(360 / 16 * r);
        }

        return (
            React.createElement("svg", {width: "0", height: "0", "aria-hidden": "true"},
                // ✔️ 已完成图标
                React.createElement("symbol", {id: "check-circle", viewBox},
                    React.createElement("circle", {fill: "currentcolor", cx: "8", cy: "8", r: "8"}),
                    React.createElement("polyline", {
                        fill: "none",
                        stroke: "var(--bg)",
                        strokeLinecap: "round",
                        strokeLinejoin: "round",
                        strokeWidth: "1.5",
                        points: "4 8,7 11,12 5"
                    })
                ),

                // ⚪ 等待图标
                React.createElement("symbol", {id: "empty-circle", viewBox},
                    React.createElement("defs", null,
                        React.createElement("rect", {id: "empty-circle-rect", x: "-1", width: "2", height: "2"})),
                    React.createElement("g", {
                        fill: "currentcolor",
                        transform: "translate(8,8)"
                    }, emptyCircleRotations.map((rotation, i) =>
                        React.createElement("use", {
                            key: i,
                            href: "#empty-circle-rect",
                            transform: `rotate(${rotation}) translate(0,6)`
                        })
                    ))
                ),

                // 🔄 进行中图标
                React.createElement("symbol", {id: "half-circle", viewBox},
                    React.createElement("clipPath", {id: "half-circle-clip"},
                        React.createElement("rect", {x: "8", y: "0", width: "8", height: "16"})),
                    React.createElement("circle", {
                        fill: "none",
                        stroke: "currentcolor",
                        strokeWidth: "2",
                        cx: "8",
                        cy: "8",
                        r: "7"
                    }),
                    React.createElement("circle", {
                        fill: "currentcolor",
                        cx: "8",
                        cy: "8",
                        r: "5",
                        clipPath: "url(#half-circle-clip)"
                    })
                )
            )
        );
    }

    // 加载进度主组件
    function Loading() {
        const [step, setStep] = useState(0); // 当前步骤索引
        const progressFrame = useRef(0); // 定时器引用

        // 初始步骤数据
        const [steps, setSteps] = useState([
            {id: 0, state: "waiting", title: "准备"},
            {id: 1, state: "waiting", title: "下载", filesPreparedMax: 50},
            {id: 2, state: "waiting", title: "分析", filesPreparedMax: 50},
            {id: 3, state: "waiting", title: "创建"},
            {id: 4, state: "waiting", title: "收尾"}
        ]);
        const stepCount = useRef(steps.length);
        const [stepObjects, setStepObjects] = useState(steps);
        // 所有步骤是否完成
        const allStepsDone = step === stepCount.current;

        /**
         * 递增文件准备或设置加载步骤的状态。
         * @param item 加载步骤
         */
        function updatedItem(item) {
            const {id, state, start, filesPrepared, filesPreparedMax} = item;
            const updated = {id, state};

            if (!start) {
                updated.start = new Date();
                updated.state = "progress";
                /// 如果没有文件需要准备，则直接标记为完成
                if (!filesPreparedMax)
                    return updated;
            }
            // 文件准备递增
            if (filesPreparedMax) {
                // 递增准备好的文件
                const prepared = filesPrepared === undefined ? -1 : filesPrepared;
                const preparedInc = 1;
                updated.filesPrepared = Math.min(prepared + preparedInc, filesPreparedMax);
            }
            // 判断是否完成
            if (!filesPreparedMax || updated.filesPrepared === filesPreparedMax) {
                // 如果没有要准备的文件或准备了所有文件，则将其标记为已完成
                updated.finish = new Date();
                updated.state = "done";
            }
            return updated;
        }

        useEffect(() => {
            const updatePromise = async (delay = 0) => await new Promise((resolve) => {
                progressFrame.current = setTimeout(resolve, delay);
            }).then(() => {
                setStepObjects((prev) => prev.map((item) => {
                    if (item.id !== step)
                        return item;
                    const updated = updatedItem(item);
                    if (updated.state === "done") {
                        clearTimeout(progressFrame.current);
                        setStep((step) => step + 1);
                    }
                    return Object.assign(Object.assign({}, item), updated);
                }));
                // loop
                if (step === 1 || step === 2)
                    updatePromise(50);
                else if (step < stepCount.current)
                    updatePromise(1500);
            });
            updatePromise();
            return () => clearTimeout(progressFrame.current);
        }, [step]);

        // 计算偏移距离
        function calculateDistance(index) {
            let distance = index;
            if (allStepsDone) {
                distance -= Math.floor(stepCount.current / 2);
            } else {
                let moveBy = step;
                if (index > step + 1) {
                    const stepHeight = 5.25;
                    moveBy += (index - (step + 1)) * (1.5 / stepHeight);
                }
                distance -= moveBy;
            }
            return distance;
        }

        return (
            React.createElement("div", {className: `loading${allStepsDone ? " loading--done" : ""}`},
                React.createElement("div", {className: "loading__steps"}, stepObjects.map((s, i) => {
                    const {state, title, start, finish, filesPrepared, filesPreparedMax} = s;
                    const distance = calculateDistance(i);
                    const fade = allStepsDone ? 0 : Math.abs(i - step);

                    return React.createElement(LoadingStepBlock, {
                        key: i,
                        title: title,
                        state: state,
                        distance: distance,
                        fade: fade,
                        start: start,
                        finish: finish,
                        filesPrepared: filesPrepared,
                        filesPreparedMax: filesPreparedMax
                    });
                }))
            )
        );
    }

    function LoadingStepBlock({
                                  state,
                                  title = "",
                                  distance = 0,
                                  fade = 0,
                                  start,
                                  finish,
                                  filesPrepared,
                                  filesPreparedMax = 0
                              }) {
        const style = {
            opacity: 1 - fade * 0.225,
            transform: `translateY(${100 * distance}%)`
        };

        // 获取图标名称
        function getIconName(state) {
            switch (state) {
                case "waiting":
                    return "empty-circle";
                case "progress":
                    return "half-circle";
                case "done":
                    return "check-circle";
                default:
                    return "empty-circle";
            }
        }

        // 获取图标颜色
        function getIconColor(state) {
            switch (state) {
                case "waiting":
                    return "neutral";
                case "progress":
                    return "warning";
                case "done":
                    return "success";
                default:
                    return "neutral";
            }
        }

        // 格式化日期
        function formatDate(date) {
            return new Intl.DateTimeFormat("zh-CN", {
                dateStyle: "medium",
                timeStyle: "medium"
            }).format(date);
        }

        return (
            React.createElement("div", {
                    className: `loading__step${state !== "waiting" ? " loading__step--in" : ""}`,
                    style: style
                },
                React.createElement(Icon, {
                    icon: getIconName(state),
                    color: getIconColor(state)
                }),
                React.createElement("div", null,
                    React.createElement("div", {className: "loading__step-title"}, title),
                    React.createElement("div", {className: "loading__step-info"},
                        start ? `${formatDate(start)} — ` : "",
                        finish ? formatDate(finish) : (state === "progress" ? React.createElement(LoadingEllipsis, null) : "")
                    ),
                    React.createElement("div", {className: "loading__step-info"},
                        filesPrepared !== undefined ? `${filesPrepared} of ${filesPreparedMax} 个文件已准备好` : ""
                    )
                )
            )
        )

        // 省略号动画组件
        function LoadingEllipsis() {
            return (
                React.createElement("div", {className: "loading__ellipsis"},
                    React.createElement("div", {className: "loading__ellipsis-dot"}, "."),
                    React.createElement("div", {className: "loading__ellipsis-dot"}, "."),
                    React.createElement("div", {className: "loading__ellipsis-dot"}, ".")
                )
            );
        }
    }
</script>
</body>
</html>
